Programming and management of experiments in oTree

An Introduction

Authors
Affiliations

Matteo Ploner

Università di Trento

Luca Congiu

Università degli Studi di Roma “Tor Vergata”

Published

July 2025

Design

Smart design


16 animali, Enzo Mari, 1957

Computerized experiments

Advantages of computerized experiments

  • Computerized experiments present several advantages over paper&pencil experiments
    • Live interaction
    • Dynamic interfaces
    • Codified data
    • Wide audience
      • Web-based experiments
        • Prolific, Mechanical Turk
  • Specialized software are available
  • Here, we focus on oTree

About oTree

  • oTree is a framework based on Python to run controlled experiments
    • Games
      • e.g., Public Goods Games (PGG)
    • Individual decision making
      • e.g., Risk elicitation tasks
    • Surveys and tests
      • e.g., Raven test
  • Support by the community
  • oTree is open-source,
    • Licensed under an adaptation of the MIT license.
      • Cite it in the paper when you use it

Code

  • Programming language of oTree is Python
    • Popular object-oriented programming language
    • Developed in the early 90’s by Guido Van Rossum
  • OTree’s user interface is based on HTML5
    • Supported by modern browsers and rich in functionalities
      • Can be enriched with
        • css
        • javascript
        • bootstrap
  • All the components of oTree are free and open-source

oTree’s architecture

  • The basic setup consists in
    1. An app (experiment) written within oTree
    2. A server computer
    • Cloud server, local PC …
    1. Subjects’ devices with a browser
    • PC, Laptop, Tablet, Mobile Phone …
  • oTree creates a session on the server and generates links for all participants
  • Participants click on the links and are sent to a personal page
    • They submit their answers, which are collected by the server
    • The experimenter can check the progress on the server

oTree app

Object hierarchy

  • oTree is based on a hierarchy of objects
    • Each object contains information about the experiment

Object hierarchy in oTree

Object hierarchy in oTree
  • A session is a series of subsessions
    • A subsession contains multiple groups
      • A group contains multiple players
        • Each player proceeds through multiple pages
  • Example:
    • 18 players take part in an experimental session made of 5 rounds and are matched in groups of 3 players. In each round, 2 pages are shown.
      • 5 subsessions
      • 6 groups
      • 18 players

Object hierarchy (ii)

  • You can access any higher-up object from a lower object
    • e.g., Player can access the Group information
      • but Group cannot access the Player information

Access of information in oTree

Access of information in oTree

Your first app

Create the projects folder

  • Choose a convenient location
    • e.g., inside DOCUMENTS
  • Create the projects folder with the following terminal command
    • Your appes will be developed here
otree startproject oTree_proj
  • You will be asked: Include sample games? (y or n):
    • I suggest y
  • Now move into your folder
cd oTree_proj
  • In the folder you will find some demo apps and other files like setting.py
  • The apps in setting.py are contained in SESSION_CONFIGS = [] and can be run locally with the command
otree devserver

Create an app

  • To create an app named hello_world move to the oTree folder
cd oTree
  • and create the app
otree startapp hello_world
  • Move to the folder my_first_app
    • You will find the following files
      • Python
        • _init_.py
      • HTML
        • MyPage.html
        • Results.html

_init_.py

  • Contains Python classes (see introductory tutorial)
  • Can be ideally divided into two parts
    • models
      • refers to main oTree’s entities
        • Constants
        • Subsession
        • Group
        • Player
      • A model is basically a database
      • Specify columns and their nature
        • Integers, strings, …
    • pages
      • refers to “screens” seen by participants
  • In older versions of oTree (<5) you could actually find models.py and pages.py in place of init

_init_.py: Content

# Import all necessary oTree modules and functions
from otree.api import *

# Documentation string describing what this app does
doc = """
Your app description
"""

#---------------------------------
# CONSTANTS
#---------------------------------
class C(BaseConstants):
    # The URL name that will appear in the browser address bar
    NAME_IN_URL = 'hello_world'
    
    # Number of players per group (None means all players in one group)
    PLAYERS_PER_GROUP = None
    
    # Number of rounds this app will run
    NUM_ROUNDS = 1

#---------------------------------
# MODELS
#---------------------------------
# Subsession: Contains all groups for one round of the experiment
class Subsession(BaseSubsession):
    pass  # No custom fields or methods defined yet

# Group: Contains multiple players who interact with each other
class Group(BaseGroup):
    pass  # No custom fields or methods defined yet

# Player: Individual participant with their data and decisions
class Player(BasePlayer):
    pass  # No custom fields or methods defined yet

#---------------------------------
# PAGES
#---------------------------------
# Main page where participants make decisions or view information
class MyPage(Page):
    pass  # No custom form fields or methods defined yet

# Wait page to synchronize all players before showing results
class ResultsWaitPage(WaitPage):
    pass  # Standard wait page with no customization

# Results page to show outcomes to participants
class Results(Page):
    pass  # No custom content defined yet

#---------------------------------
# ORDER OF PAGES
#---------------------------------
# Defines the sequence of pages participants will see
page_sequence = [MyPage, ResultsWaitPage, Results]

Templates


<!-- Define the title block that will appear in the browser tab and page header -->
{{ block title }}
    Page title
{{ endblock }}

<!-- Define the main content block that contains the page body -->
{{ block content }}

    <!-- Automatically display all form fields defined in the Page class -->
    {{ formfields }}
    
    <!-- Display the standard "Next" button to proceed to the next page -->
    {{ next_button }}

{{ endblock }}
  • These are the pages that are displayed to participants
    • html files that contain informations and forms
      • forms are used to collect data
    • A default MyPage.html is created
  • The HTML can rely on “fancy” stuff
    • Javascript
    • Bootstrap framework
    • CSS

Modify “MyPage.html”

  • Add the following title “Hello World!”

  • Add a smiley 😜


<!-- Set a custom title for this page -->
{{ block title }}
    Hello World!
{{ endblock }}

<!-- Define the main content area -->
{{ block content }}

    <!-- Display any form fields defined in the Page class -->
    {{ formfields }}
    <!-- Show the standard Next button -->
    {{ next_button }}

<!-- Add custom HTML content: a large smiley emoji (Unicode character) -->
<p style="font-size:200px">&#128540;</p>

    <!-- Note: This duplicates formfields and next_button - usually not needed -->
    {{ formfields }}
    {{ next_button }}

{{ endblock }}

Forms

Submitting information

  • Each page in oTree can contain a form
    • The player fills with some value and then submits it
      • Cardinal values
        • Integer, Float
      • Ordinal values
        • Integer, Categorical
      • Text
        • Strings of numbers and letters
  • Several formats to collect values
    • Open fields
    • Buttons
    • Radio buttons
    • Dropdown lists

Basic structure of forms

  • First create fields in __init__.py, models section
  • As an example, if you want to collect name and age
class Player(BasePlayer):
    name = models.StringField(label="Your name:")
    age = models.IntegerField(label="Your age:")
  • Then in *__init__py*, pages section, you will create a class
class Anag(Page):
    form_model = 'player'
    form_fields = ['name', 'age'] # saved as player.name, player.age
  • Finally, in a template Anag.html the form will be displayed with
{{ formfields }}
  • To display the forms separately
{{ formfields player.age }}

Input formats

Examples

  • In the following examples the participant receives a random number
  • The participant is asked to report features of the number
    • Several input interfaces are presented
      • Radio
      • Button
      • Checker
      • Dropdown
      • Radio Sequence
      • Text
      • Value
      • Tabular
      • Sliders

Radio

  • __init.py__ (Models)
input_radio = models.CharField(
    choices=['Odd', 'Even'],
    widget=widgets.RadioSelect)
  • __init.py__ (Pages)
class Radio(Page):
    form_model = 'player'
    form_fields = ['input_radio']
  • Template
{{ block content }}

{{ formfield player.input_radio label="The number is:"}}

  {{ next_button }}

{{ endblock }}

Button

  • __init.py__ (Models)
class Player(BasePlayer):
    input_button = models.CharField()
  • __init.py__ (Pages)
class Button(Page):
    form_model = 'player'
    form_fields = ['input_button']
  • Template

    • This widget does not rely on oTree functionalities
    • Raw HTML with bootstrap features
{{ block content }}

The number is:
<button name="input_button" value="Even" class="btn btn-outline-primary" btn-lg>Even</button>
<button name="input_button" value="Odd" class="btn btn-outline-primary" btn-lg>Odd</button>

{{ endblock }}

Checker

  • __init.py__ (Models)
class Player(BasePlayer):
  input_checker = models.CharField(
    choices=[
            ["Even", 'Yes'],
            ["Odd", 'No']
            ],
    widget=widgets.RadioSelectHorizontal,
    label="Is the number even?")
  • __init.py__ (Pages)
class Checker(Page):
    form_model = 'player'
    form_fields = ['input_checker']
  • Template

{{ block content }}

{{ formfield player.input_checker }}

    {{ next_button }}

{{ endblock }}

Radiosequence

  • __init.py__ (Models)
class Player(BasePlayer):
    input_radiosequence = models.IntegerField(
        choices=[1, 2, 3, 4, 5, 6, 7, 8, 9],
        widget=widgets.RadioSelectHorizontal
        )
  • __init.py__ (Pages)
class RadioSequence(Page):
    form_model = 'player'
    form_fields = ['input_radiosequence']
  • Template

{{ block content }}

{{ formfield player.input_radiosequence label="The number is:"}}

    {{ next_button }}

{{ endblock }}

Text

  • __init.py__ (Models)
class Player(BasePlayer):
    input_text = models.CharField()
  • __init.py__ (Pages)
class Text(Page):
    form_model = 'player'
    form_fields = ['input_text']
  • Template

{{ block content }}

{{ formfield player.input_text label="The number is:"}}

    {{ next_button }}

{{ endblock }}

Value

  • __init.py__ (Models)
class Player(BasePlayer):
    input_value = models.IntegerField(min=1, max=9)
  • __init.py__ (Pages)
class Value(Page):
    form_model = 'player'
    form_fields = ['input_value']
  • Template

{{ block content }}

{{ formfield player.input_value label="The number is:"}}

    {{ next_button }}

{{ endblock }}

Tabular

  • __init.py__ (Models)
class Player(BasePlayer):
    tab_1 = models.BooleanField(blank=True)
    tab_2 = models.BooleanField(blank=True)
    tab_3 = models.BooleanField(blank=True)
    tab_4 = models.BooleanField(blank=True)
    tab_5 = models.BooleanField(blank=True)
    tab_6 = models.BooleanField(blank=True)
    tab_7 = models.BooleanField(blank=True)
    tab_8 = models.BooleanField(blank=True)
    tab_9 = models.BooleanField(blank=True)
  • __init.py__ (Pages)
class Tabular(Page):
    form_model = 'player'
    form_fields = ['tab_1','tab_2','tab_3','tab_4','tab_5','tab_6','tab_7','tab_8','tab_9']
  • Template
{{ block content }}

  <table class="table table-bordered">
    <tbody>
      <tr>
          <td><button name="tab_1" value="True" class="btn btn-outline-primary" btn-lg"> 1 </button></td>
          <td> <button name="tab_2" value="True" class="btn btn-outline-primary" btn-lg">2</button></td>
          <td> <button name="tab_3" value="True" class="btn btn-outline-primary" btn-lg">3</button></td>
        </tr>
        <tr>
          <td><button name="tab_4" value="True" class="btn btn-outline-primary" btn-lg">4</button></td>
          <td><button name="tab_5" value="True" class="btn btn-outline-primary" btn-lg">5</button></td>
          <td><button name="tab_6" value="True" class="btn btn-outline-primary" btn-lg">6</button></td>
      </tr>
      <tr>
        <td ><button name="tab_7" value="True" class="btn btn-outline-primary" btn-lg">7</button></td>
        <td><button name="tab_8" value="True" class="btn btn-outline-primary" btn-lg">8</button></td>
        <td><button name="tab_9" value="True" class="btn btn-outline-primary" btn-lg">9</button></td>
      </tr>
    </tbody>
  </table>

{{ endblock }}

Slider (simple)

  • __init.py__ (Models)
input_slider = models.IntegerField(min=1, max=9)
  • __init.py__ (Pages)
class Slider(Page):
    form_model = 'player'
    form_fields = ['input_slider']
  • Template

<div class="input-group">
    <div class="input-group-prepend">
        <span class="input-group-text">Disagree</span>
    </div>

    <input type="range" name="input_slider" min="1" max="9" step="1">

    <div class="input-group-append">
        <span class="input-group-text">Agree</span>
    </div>
</div> 

{{ form.input_slider.errors }}

Sliders (boosted)

  • Relies on css styling + javascript code

Hidden Initial State: No visible thumb until first interaction

Click-to-Activate: Pointer appears on click, value calculated from click position

Real-Time Display: Shows selected value (0-100) with continuous updates

Cross-Platform: Works on desktop and mobile devices

Enhanced Feedback: Smooth transitions and visual styling changes

  • Template
{{ block title }}
    Slider Plus
{{ endblock }}

{{ block content }}
<div class="slider-container">
    <!-- Label for accessibility and instructions -->
    <label for="input_slider">Please select a value between 0 and 100:</label>
    <div class="slider-wrapper">
        <div class="slider-with-labels">
            <!-- Left label for minimum value -->
            <span class="slider-label-left">0</span>
            <!-- The actual slider input, styled and controlled by JS/CSS -->
            <input type="range" 
                   id="input_slider" 
                   name="input_slider" 
                   min="0" 
                   max="100" 
                   step="1" 
                   class="custom-slider">
            <!-- Right label for maximum value -->
            <span class="slider-label-right">100</span>
        </div>
        <!-- Area to display the selected value or instructions -->
        <div id="slider-value" class="slider-value">Click on the slider to select a value</div>
    </div>
</div>

<!-- Error display for form validation (commented out, can be enabled if needed) -->
<!-- {{ form.input_slider.errors }} -->

<br>
<!-- Next button container, hidden by default and shown only after a value is selected -->
<div id="next-button-container" style="display: none;">
    {{ next_button }}
</div>

{{ endblock }}

Sliders (boosted) - CSS and JS

  • The slider relies on a custom code in the template
    • The code is written in the scripts block

{{ block scripts }}
{{ block scripts }}
<script>
// Wait for the DOM to be fully loaded before running the script
document.addEventListener('DOMContentLoaded', function() {
    // Get references to slider, value display, and next button container
    const slider = document.getElementById('input_slider');
    const valueDisplay = document.getElementById('slider-value');
    const nextButtonContainer = document.getElementById('next-button-container');
    let hasBeenClicked = false; // Track if slider has been activated
    
    // Hide the slider value initially (no value selected)
    slider.value = '';
    
    // Update the value display and next button visibility
    function updateValueDisplay(value) {
        if (value !== '') {
            valueDisplay.textContent = `Selected value: ${value}`;
            valueDisplay.classList.add('has-value');
            // Show the next button with animation
            nextButtonContainer.style.display = 'block';
            setTimeout(() => {
                nextButtonContainer.classList.add('show');
            }, 10);
        } else {
            valueDisplay.textContent = 'Click on the slider to select a value';
            valueDisplay.classList.remove('has-value');
            // Hide the next button
            nextButtonContainer.classList.remove('show');
            nextButtonContainer.style.display = 'none';
        }
    }
    
    // Handle the first mouse click on the slider
    function handleFirstInteraction(event) {
        if (!hasBeenClicked) {
            hasBeenClicked = true;
            slider.classList.add('activated'); // Show the thumb
            
            // Calculate where the user clicked and set slider value accordingly
            const rect = slider.getBoundingClientRect();
            const percent = (event.clientX - rect.left) / rect.width;
            const value = Math.round(percent * 100);
            const clampedValue = Math.max(0, Math.min(100, value));
            slider.value = clampedValue;
            updateValueDisplay(clampedValue);
        }
    }
    
    // Handle the first touch on mobile devices
    function handleFirstTouch(event) {
        if (!hasBeenClicked) {
            hasBeenClicked = true;
            slider.classList.add('activated');
            
            // Use touch coordinates to set slider value
            const touch = event.touches[0];
            const rect = slider.getBoundingClientRect();
            const percent = (touch.clientX - rect.left) / rect.width;
            const value = Math.round(percent * 100);
            const clampedValue = Math.max(0, Math.min(100, value));
            slider.value = clampedValue;
            updateValueDisplay(clampedValue);
        }
    }
    
    // Listen for first interaction (mouse or touch)
    slider.addEventListener('mousedown', handleFirstInteraction);
    slider.addEventListener('touchstart', handleFirstTouch);
    
    // After activation, update value display on slider movement
    slider.addEventListener('input', function() {
        if (hasBeenClicked) {
            updateValueDisplay(this.value);
        }
    });
    
    // Prevent slider from working before activation
    slider.addEventListener('input', function(event) {
        if (!hasBeenClicked) {
            event.preventDefault();
            this.value = '';
        }
    });
    
    // Initialize the display in the "not yet clicked" state
    updateValueDisplay('');
});
</script>
{{ endblock }}
  • The slider relies on a custom CSS code in the template
    • The code is written in the styles block

{{ block styles }}
<style>
    /* Main container for the entire slider component */
    .slider-container {
        margin: 20px 0;      /* Adds vertical spacing above and below */
        text-align: center;  /* Centers all content horizontally */
    }
    
    /* Wrapper for the slider and its associated elements */
    .slider-wrapper {
        margin: 20px 0;      /* Adds vertical spacing */
        position: relative;  /* Allows absolute positioning of children if needed */
    }
    
    /* Flex container for slider and its labels */
    .slider-with-labels {
        display: flex;       /* Horizontal layout for children */
        align-items: center; /* Vertically center items */
        gap: 10px;           /* Space between slider and labels */
        margin: 10px 0;      /* Vertical spacing */
    }
    
    /* Styling for the "0" and "100" labels */
    .slider-label-left,
    .slider-label-right {
        font-weight: bold;   /* Bold text */
        font-size: 16px;     /* Medium size */
        color: #666;         /* Gray color */
        min-width: 30px;     /* Minimum width for alignment */
        text-align: center;  /* Center text */
    }
    
    /* Main slider input styling */
    .custom-slider {
        flex: 1;                        /* Expands to fill space between labels */
        height: 8px;                    /* Thin track */
        border-radius: 5px;             /* Rounded corners */
        background: #ddd;               /* Light gray track */
        outline: none;                  /* No focus outline */
        opacity: 0.7;                   /* Slightly transparent */
        transition: opacity 0.2s;       /* Smooth opacity change */
        -webkit-appearance: none;       /* Remove default browser styling (WebKit) */
        appearance: none;               /* Remove default browser styling (standard) */
    }
    
    /* Slider becomes fully opaque on hover */
    .custom-slider:hover {
        opacity: 1;
    }
    
    /* Hide the slider thumb initially (WebKit browsers) */
    .custom-slider::-webkit-slider-thumb {
        -webkit-appearance: none;
        appearance: none;
        width: 0;
        height: 0;
        background: transparent;
        cursor: pointer;
        transition: all 0.3s ease;
    }
    
    /* Hide the slider thumb initially (Firefox) */
    .custom-slider::-moz-range-thumb {
        width: 0;
        height: 0;
        background: transparent;
        cursor: pointer;
        border: none;
        transition: all 0.3s ease;
    }
    
    /* Show the thumb after activation (WebKit browsers) */
    .custom-slider.activated::-webkit-slider-thumb {
        width: 20px;
        height: 20px;
        background: #04AA6D;           /* Green thumb */
        border-radius: 50%;            /* Circular */
        box-shadow: 0 0 4px rgba(0,0,0,0.3); /* Subtle shadow */
    }
    
    /* Show the thumb after activation (Firefox) */
    .custom-slider.activated::-moz-range-thumb {
        width: 20px;
        height: 20px;
        background: #04AA6D;
        border-radius: 50%;
        box-shadow: 0 0 4px rgba(0,0,0,0.3);
        border: none;
    }
    
    /* Style the slider track (WebKit browsers) */
    .custom-slider::-webkit-slider-track {
        width: 100%;
        height: 8px;
        cursor: pointer;
        background: #ddd;
        border-radius: 5px;
    }
    
    /* Style the slider track (Firefox) */
    .custom-slider::-moz-range-track {
        width: 100%;
        height: 8px;
        cursor: pointer;
        background: #ddd;
        border-radius: 5px;
        border: none;
    }
    
    /* Styling for the value display box */
    .slider-value {
        margin-top: 15px;
        font-size: 18px;
        font-weight: bold;
        color: #333;
        min-height: 25px;
        padding: 10px;
        background-color: #f8f9fa;
        border: 2px solid #e9ecef;
        border-radius: 8px;
        display: inline-block;
        min-width: 200px;
    }
    
    /* Highlight value display when a value is selected */
    .slider-value.has-value {
        color: #04AA6D;
        border-color: #04AA6D;
        background-color: #f0fff4;
    }
    
    /* Container for the next button, initially hidden and faded out */
    #next-button-container {
        margin-top: 20px;
        opacity: 0;
        transition: opacity 0.3s ease-in-out;
    }
    
    /* Show the next button with fade-in */
    #next-button-container.show {
        opacity: 1;
    }
</style>
{{ endblock }}

–>

Steps to implement the survey

  • Implement a simple survey to collect the age of course participants
  1. Develop the oTree code
    • __init__.py
    • templates
  2. Test our code locally
  3. Transfer our code to a server (running oTree)
  4. Send links to participants (you)
  5. Fill in the survey
  6. Collect outcomes and analyze them

__init__.py: models

from otree.api import *

author = 'MP'

doc = """
A simple app to collect the age of respondent
"""

class C(BaseConstants):
    NAME_IN_URL = 'my_first_survey'
    PLAYERS_PER_GROUP = None
    NUM_ROUNDS = 1

class Subsession(BaseSubsession):
    pass

class Group(BaseGroup):
    pass

class Player(BasePlayer):
    age = models.IntegerField(choices=range(18, 99, 1))
  • Create the field age in class player
    • The input will be an integer spanning 18-99

__init__.py: pages

class CollectAge(Page):
    form_model = 'player'
    form_fields = ['age']


class Results(Page):
  pass
        
page_sequence = [CollectAge, Results]
  • In page CollectAge we insert a form for class player
    • The field is age
      • Must be the same name we used in models.py
        • age and not, as an example, Age
  • In page Results we give a feedback
    • your_age will be printed on the screen
  • page_sequence defines the sequence of your pages
    • All your pages are a class that must be defined here

Develop the oTree code: templates

  • Templates are the .html files
  • CollectAge

{{ block title }}
    Insert your age here
{{ endblock }}

{{ block content }}

{{ formfield player.age }}

    {{ next_button }}


{{ endblock }}
  • { formfield player.age } is where the information is input
    • Variable age in the class player
  • Aesthetics elements
    • Title
    • Body
    • Next button

Develop the oTree code: templates (ii)

  • Results
{{ block title }}
    Your age
{{ endblock }}

{{ block content }}
Your age is {{player.age}}. Thank you for answering!
    {{ next_button }}
{{ endblock }}
  • { player.age } is passed gathered from class player
    • The right name must be put within { } brackets

Test our code locally

  • See also the video tutorial!

  • Move to the folder in which your oTree is installed

    • Usually cd ~/oTree
    • Add your new app to the `settings.py file
SESSION_CONFIGS = [
    dict(
        name='my_first_survey',
        display_name='my_first_survey',
        num_demo_participants=4,
        app_sequence=['my_first_survey'],
    )
]
  • In app_sequence the exact name of the apps should be given
  • Give command
otree devserver

Test our code locally (ii)

  • Open a browser and insert http://localhost:8000/

  • Click on the name of your app

  • Click on the session-wide link and try it

Appendix

Assignment 1

    1. Consider the class Pokemon (see below)
    • Print the name of p_1
    • Print the ability of p_1
    1. Create a new pokemon (instance) named Charizard
    • Charizard: Height: 5’ 07”; Weight: 199.5 lbs; Category: Flame
    • Check whether Charizard is taller than Bulbasaur

Assignment

  • Create the my_first_survey app
    • Add the field gender
      • Categorical variable with values: Male, Female, Non-binary, Prefer not to answer
    • Collect gender both with
      • Radio buttons
      • Dropdown list

Python Basics

Basics: Numbers and logical operators

#####################################################
# Numbers and logical operators
####################################################

# Math is what you would expect
1 + 1   # => 2
8 - 1   # => 7
10 * 2  # => 20
35 / 5  # => 7.0

# Enforce precedence with parentheses
1 + 3 * 2  # => 7
(1 + 3) * 2  # => 8


# Boolean Operators
## Note "and" and "or" are case-sensitive
True and False  # => False
False or True   # => True

## True and False are actually 1 and 0 but with different keywords
True + True # => 2
True * 8    # => 8
False - 5   # => -5

# Comparisons

## Equality is ==
1 == 1  # => True
2 == 1  # => False

## Inequality is !=
1 != 1  # => False
2 != 1  # => True

## More comparisons
1 < 10  # => True
1 > 10  # => False
2 <= 2  # => True
2 >= 2  # => True

Basics: Strings and variables

  • Strings are an ordered collection of characters

# Strings are created with " or '

"This is a string."

'This is also a string.'

# Strings can be added too! But try not to do this.
"Hello " + "world!"  # => "Hello world!"

# You can also format using f-strings or formatted string literals (in Python 3.6+)
name = "Reiko"
f"{name} is {len(name)} characters long." # => "Reiko is 5 characters long."

# Strings can be sliced and indexed

name[0] => "R"
name[-2]  => "k"


# There are no declarations, only assignments.
# Convention is to use lower_case_with_underscores
some_var = 5
some_var  # => 5

Basics: Lists and tuples

  • Lists are ordered collections of arbitrary items
    • Can contain numbers, strings, and other lists
    • Accessed by offset
      • As an example, all players in a group
# empty list
li = []

# Prefilled list
other_li = [4, 5, 6]

# Add stuff to the end of a list with append
other_li.append(7)    # => li is now [4, 5, 6, 7]

# Remove from the end with pop
other_li.pop()        # => 7 li is now [4, 5, 6]

# Access a list like you would any array
other_li [0]   # => 4

# Look at the last element
other_li [-1]  # => 6

# You can look at ranges with slice syntax.
# The start index is included, the end index is not
# (It's a closed/open range for you mathy types.)
other_li [1:3]   # Return list from index 1 to 3 => [5, 6]

# Remove arbitrary elements from a list with "del"
del other_li[2]  # li is now [4, 5]

# Insert an element at a specific index
other_li.insert(1, 2)  # li is now [4, 2, 5] again

# Get the index of the first item found matching the argument
other_li.index(2)  # => 1

# Check for existence in a list with "in"
1 in other_li  # => False

# Examine the length with "len()"
len(other_li)  # => 3

# Tuples are like lists but are immutable.
tup = (1, 2, 3)
tup[0]      # => 1

Basics: Dictionaries

  • Useful way to collect and organize information
    • As an example, payoffs of players
# Dictionaries store mappings from keys to values

empty_dict = {}

# Here is a prefilled dictionary
filled_dict = {"one": 1, "two": 2, "three": 3}

# Look up values with []
filled_dict["one"]  # => 1

# Get all keys as an iterable with "keys()". We need to wrap the call in list()
# to turn it into a list. Order of insertion is preserved (in Python >=3.7)

list(filled_dict.keys())  # => ["one", "two", "three"]


# Get all values as an iterable with "values()". Once again we need to wrap it
# in list() to get it out of the iterable.

list(filled_dict.values())  # => [1, 2, 3]

# Check for existence of keys in a dictionary with "in"
"one" in filled_dict  # => True
1 in filled_dict      # => False

# Adding to a dictionary
filled_dict.update({"four":4})  # => {"one": 1, "two": 2, "three": 3, "four": 4}
filled_dict["four"] = 4         # another way to add to dict

# Remove keys from a dictionary with del
del filled_dict["one"]  # Removes the key "one" from filled dict

Basics: Dictionaries in oTree

  • File setting.py in oTree controls the apps that are run
    • Apps are dictionaries contained in a list SESSION_CONFIGS
      • keys: name, display_name, num_demo_participants, app_sequence, …
SESSION_CONFIGS = [
    dict(
        name='my_first_survey',
        display_name='my_first_survey',
        num_demo_participants=4,
        app_sequence=['my_first_survey'],
    ),
    dict(
        name='self',
        display_name='importance_of_self',
        num_demo_participants=4,
        app_sequence=['what_is_self'],
    )
]

SESSION_CONFIGS[0].keys()
  • dict_keys([‘name’, ‘display_name’, ‘num_demo_participants’, ‘app_sequence’])
SESSION_CONFIGS[0].values()
  • dict_values([‘my_first_survey’, ‘my_first_survey’, 4, [‘my_first_survey’]])

Basics: Control Flow and Iteration

  • Many times you need to iterate through variables
    • As an example, collect all choice sof participants in a group
# Let's just make a variable
some_var = 5

# Here is an if statement. Indentation is significant in Python!
# Convention is to use four spaces, not tabs.
# This prints "some_var is smaller than 10"

# if conditions

if some_var > 10:
    print("some_var is totally bigger than 10.")
elif some_var < 10:    # This elif clause is optional.
    print("some_var is smaller than 10.")
else:                  # This is optional too.
    print("some_var is indeed 10.")

# loops

for i in range(4):
    print(i)
# =>0
# =>1
# =>2
# =>3

for animal in ["dog", "cat", "mouse"]:
    # You can use format() to interpolate formatted strings
    print("{} is a mammal".format(animal))
# =>dog is a mammal
# =>cat is a mammal
# =>mouse is a mammal

# We can use list comprehensions to loop or filter
numbers = [3, 4, 5, 6, 7]
[x for x in numbers if x > 5]   # => [6, 7]
  • Indenting is important!

Basics: Functions

  • Functions are a device that groups a bunch of statements

# Use "def" to create new functions
def add(x, y):
    print("x is {} and y is {}".format(x, y))
    return x + y  # Return values with a return statement

# Calling functions with parameters
add(5, 6)  # => prints out "x is 5 and y is 6" and returns 11

Basics: Classes

  • Classes are main object-oriented-programming (OOP) in Python
    • Class objects provide default behavior
      • The class statement creates a class object and assigns it a name
      • Assignments inside class statements make class attributes
      • Class attributes export object state and behavior
        • def statements inside class generate a method
    • Instance objects are generated from classes
      • Calling a class object makes a new instance object
      • Each instance object inherits class attributes and gets its own namespace
      • Assignments to self in methods make per-instance attributes
        • self refers to the instance being processed

Classes: An Example

  • Pokemon is a class with some properties
    • Height
    • Weight
    • Category
  • Two instances of the class →

Classes: An Example (ii)

class Pokemon:
    nature = "Pokemon"# this property is shared by all instances
    def __init__(self, name, height, weight, category):
            # Assign the argument to the instance's name attribute
            self.name = name
            self.height = height
            self.weight = weight
            self.category = category
            self.comment=[]
    # to add a comment
    def add_comment(self, comment):
        self.comment.append(comment)
    # convert height and weight into metric
    def convert_metric(self):
        # conversion rates
        feet_conv_cm=30.5
        inch_conv_cm=2.54
        lbs_conv_kg=0.453592
        self.height_cm = int(self.height.split("'")[0])*feet_conv_cm+int(self.height.split("'")[1])*inch_conv_cm
        self.weight_kg = self.weight*lbs_conv_kg
        # print the result
        print("Height (cm): %f, Weight (Kg): %f" %(self.height_cm, self.weight_kg))

Classes: An Example (iii)


# Create two instances of the class
p_1 = Pokemon("Charmander","2' 5''", 18.7, "Lizard")
p_2 = Pokemon("Bulbasaur","2' 04''", 15.2, "Seed")

# Add a comment
p_1.add_comment("Its ability is Blaze")
p_2.add_comment("Its ability is Overgrow")

# Convert their measures in metric system
p_1.convert_metric()
p_2.convert_metric()

# Who is taller?
if p_1.height>p_2.height:
    print(p_1.name + " is taller")
elif p_1.height<p_2.height:
    print(p_2.name + " is taller")
else:
    print(p_2.name + " and" + p_1.name + "are equally taller")

References

References

Ascher, David, and Mark Lutz. 1999. Learning Python. O’Reilly.
Chen, Daniel L, Martin Schonger, and Chris Wickens. 2016. “oTree—an Open-Source Platform for Laboratory, Online, and Field Experiments.” Journal of Behavioral and Experimental Finance 9: 88–97.
Fischbacher, Urs. 2007. “Z-Tree: Zurich Toolbox for Ready-Made Economic Experiments.” Experimental Economics 10 (2): 171–78.
Holzmeister, Felix. 2017. “oTree: Ready-Made Apps for Risk Preference Elicitation Methods.” Journal of Behavioral and Experimental Finance 16: 33–38.